DIRECTORY Atom USING [GetPName], BlackCherry USING [AddDisplayerProc, MsgHandle, MsgSetInfo, GetMsgContents, GetMsgID, ProcessNewMailProc, InsertMsgsProc, MsgButtonTextProc, CustomProcs, CustomProcsRec, RegisterCustomProcs, Report], Convert USING [IntFromRope], IO USING [atom, int, PutFR, RIS, rope, RopeFromROS, ROS, STREAM], LoganBerryEntry USING [GetAllAttrs, GetAttr], LoganQuery USING [AttributePattern, AttributePatternRec, AttributePatterns, WriteAttributePatterns], Menus USING [MenuProc], PopUpSelection USING [Request], Rope USING [Cat, Concat, Find, Substr, ROPE], SimMatch USING [Tokenize], TapFilter USING [AddFilter, Agent, Annotation, CreateAgent, DeleteFilter, Error, ExistsFilter, GetAnnotations, IsAgentIdle, LookupFilter, MonitorAgent, MonitorProc, ParseMsgIntoFields, Query, WakeupAgent], TapMsgQueue USING [EntryFromMsg, Msg, MsgQueue, Put, Create], UserProfile USING [CallWhenProfileChanges, ProfileChangedProc, Token], ViewerOps USING [FetchProp]; TapInBlackCherry: CEDAR PROGRAM IMPORTS Atom, BlackCherry, Convert, IO, LoganBerryEntry, LoganQuery, PopUpSelection, Rope, SimMatch, TapFilter, TapMsgQueue, UserProfile, ViewerOps ~ BEGIN ROPE: TYPE ~ Rope.ROPE; STREAM: TYPE ~ IO.STREAM; MsgHandle: TYPE ~ BlackCherry.MsgHandle; MsgSetInfo: TYPE ~ BlackCherry.MsgSetInfo; debugging: BOOL _ FALSE; userName: ROPE _ NIL; filterDBName: ROPE _ "TapFiltersLB.df"; annotationDBName: ROPE _ "TapAnnotationsLB.df"; checkProfile: BOOLEAN _ TRUE; MsgData: TYPE ~ REF MsgDataRec; -- for caching info in BlackCherry.MsgHandle MsgDataRec: TYPE ~ RECORD [ ilevel: INT _ -1, parsedMsg: TapMsgQueue.Msg _ NIL ]; filteringAgent: TapFilter.Agent _ NIL; filterFeeder: TapMsgQueue.MsgQueue; defaultILevel: INT _ 50; newMail: BOOLEAN _ FALSE; tapProcs: BlackCherry.CustomProcs _ NEW[BlackCherry.CustomProcsRec _ [newMail: FilterMessages, insertMsgs: AddInInterestOrder, msgButtonText: TOCWithInterestLevel]]; TOCWithInterestLevel: BlackCherry.MsgButtonTextProc ~ { text _ IO.PutFR["%3g %g", IO.int[GetMsgILevel[msgH]], IO.rope[msgH.toc]]; }; AddInInterestOrder: BlackCherry.InsertMsgsProc ~ { IF msgH = NIL THEN RETURN; SELECT TRUE FROM msInfo.first=NIL => { -- first batch of msgs msInfo.first _ SortMsgsIntoMsgs[unsorted: msgH, sorted: NIL]; msInfo.last _ msInfo.first; }; NOT newMail => { -- insert msgs into message set msInfo.first _ SortMsgsIntoMsgs[unsorted: msgH, sorted: msInfo.first]; }; ENDCASE => { -- append sorted msgs to end of message set msInfo.last.next _ SortMsgsIntoMsgs[unsorted: msgH, sorted: NIL]; }; WHILE msInfo.last.next # NIL DO -- update pointer to last message msInfo.last _ msInfo.last.next; ENDLOOP; newMail _ FALSE; }; FilterMessages: BlackCherry.ProcessNewMailProc ~ { ENABLE TapFilter.Error => { BlackCherry.Report["Problem with filtering agent: %g - %g.\n", IO.atom[ec], IO.rope[explanation]]; CONTINUE; }; IF filteringAgent = NIL THEN { filterFeeder _ TapMsgQueue.Create[]; GetProfileInfo[]; filteringAgent _ TapFilter.CreateAgent[feeder: filterFeeder, filterDB: filterDBName, user: NIL, annotDB: annotationDBName]; IF filteringAgent = NIL THEN RETURN; TapFilter.MonitorAgent[agent: filteringAgent, proc: ReportProgress]; }; BlackCherry.Report["\nAnnotating messages: "]; FOR new: MsgHandle _ msgH, new.next WHILE new # NIL DO newData: MsgData _ NEW[MsgDataRec]; contents: ROPE _ BlackCherry.GetMsgContents[msInfo, new].contents; new.data _ newData; newData.parsedMsg _ TapFilter.ParseMsgIntoFields[contents]; IF new.gvID = NIL THEN new.gvID _ BlackCherry.GetMsgID[msInfo, new]; newData.parsedMsg _ CONS[[$MsgID, new.gvID], newData.parsedMsg]; TapMsgQueue.Put[newData.parsedMsg, filterFeeder]; ENDLOOP; TapFilter.WakeupAgent[filteringAgent]; [] _ TapFilter.IsAgentIdle[agent: filteringAgent, wait: TRUE]; BlackCherry.Report[" done.\n"]; newMail _ TRUE; }; ReportProgress: TapFilter.MonitorProc = { BlackCherry.Report["@"]; }; GetMsgILevel: PROC [msgH: MsgHandle] RETURNS [ilevel: INT] ~ { ENABLE TapFilter.Error => { BlackCherry.Report["Problem with annotation database: %g - %g.\n", IO.atom[ec], IO.rope[explanation]]; CONTINUE; }; Max: PROC [values: LIST OF ROPE] RETURNS [max: INT] ~ { max _ 0; FOR rL: LIST OF ROPE _ values, rL.rest WHILE rL # NIL DO i: INT _ Convert.IntFromRope[rL.first]; IF i > max THEN max _ i; ENDLOOP; }; msgData: MsgData; IF msgH.data = NIL THEN msgH.data _ NEW[MsgDataRec]; msgData _ NARROW[msgH.data]; IF msgData.ilevel < 0 THEN { -- get ilevel from database and cache for future use annot: TapFilter.Annotation; IF msgH.gvID = NIL THEN msgH.gvID _ BlackCherry.GetMsgID[msgH.msInfo, msgH]; GetProfileInfo[]; annot _ TapFilter.GetAnnotations[annotDB: annotationDBName, msgID: msgH.gvID]; msgData.ilevel _ IF annot = NIL THEN defaultILevel ELSE Max[LoganBerryEntry.GetAllAttrs[entry: annot, type: $Level]]; }; ilevel _ msgData.ilevel; }; SortMsgsIntoMsgs: PROC [unsorted, sorted: MsgHandle] RETURNS [new: MsgHandle] ~ { WHILE unsorted # NIL DO msg: MsgHandle _ unsorted; unsorted _ unsorted.next; sorted _ InsertMsg[msg, sorted]; ENDLOOP; RETURN[sorted]; }; InsertMsg: PROC [msg: MsgHandle, sorted: MsgHandle] RETURNS [new: MsgHandle] ~ { prev: MsgHandle _ NIL; ilevel: INT _ GetMsgILevel[msg]; msg.next _ NIL; IF sorted = NIL THEN RETURN [msg]; new _ sorted; FOR each: MsgHandle _ sorted, each.next WHILE each # NIL DO IF ilevel > GetMsgILevel[each] THEN { -- found place for insertion msg.next _ each; IF prev = NIL THEN new _ msg ELSE prev.next _ msg; EXIT; }; prev _ each; ENDLOOP; IF msg.next = NIL THEN -- add to end of list prev.next _ msg; }; menuItems: LIST OF ROPE _ LIST["InterestLevel?", "DropConv", "BoostConv", "DropSim", "BoostSim"]; menuItemsDoc: LIST OF ROPE _ LIST[ "Explain why msg has given interest level", "Drop msg's conversation to a low interest level", "Raise msg's conversation to a high interest level", "Drop similar msgs to a low interest level", "Raise similar msgs to a high interest level" ]; subMenuItems: LIST OF ROPE _ LIST["5", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55", "60", "65", "70", "75", "80", "85", "90", "95", "default", "original"]; subMenuItemsDoc: LIST OF ROPE _ LIST["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "Set to default value?", "Set to original value?"]; dropLevel: ROPE _ "25"; boostLevel: ROPE _ "75"; defaultSimInterest: ROPE _ "50"; defaultSimThreshold: ROPE _ "50"; FiltersMenuProc: Menus.MenuProc ~ { ENABLE { UNWIND => NULL; TapFilter.Error => {BlackCherry.Report["Filters problem: %g - %g.\n", IO.atom[ec], IO.rope[explanation]]; CONTINUE;}; }; dropSimInterest: ROPE; dropSimThreshold: ROPE; msInfo: MsgSetInfo ~ NARROW[ViewerOps.FetchProp[NARROW[parent], $BlackCherry]]; which: INT _ PopUpSelection.Request[header: "Filters", choice: menuItems, headerDoc: NIL, choiceDoc: menuItemsDoc, default: 0, timeOut: 15]; IF which <= 0 THEN RETURN; -- no selection dropSimInterest _ dropLevel; dropSimThreshold _ dropLevel; SELECT which FROM 1 => ExplainILevel[msInfo]; 2 => DropConv[msInfo]; 3 => BoostConv[msInfo]; 4 => DropSim[msInfo]; 5 => BoostSim[msInfo] ENDCASE; }; AnnotationToRope: PROC [note: TapFilter.Annotation] RETURNS [rope: Rope.ROPE] = { rope _ NIL; FOR l: TapFilter.Annotation _ note, l.rest WHILE l # NIL DO rope _ IO.PutFR["%g %g: \"%g\"", IO.rope[rope], IO.rope[Atom.GetPName[l.first.type]], IO.rope[l.first.value]]; ENDLOOP; }; ExplainILevel: PROC [msInfo: MsgSetInfo] ~ { note: TapFilter.Annotation; IF msInfo.selected = NIL THEN { BlackCherry.Report["\nNo message selected.\n"]; RETURN; }; GetProfileInfo[]; BlackCherry.Report["\nAnnotations for message %g: ", IO.rope[msInfo.selected.gvID]]; note _ TapFilter.GetAnnotations[annotDB: annotationDBName, msgID: msInfo.selected.gvID]; BlackCherry.Report["%g\n", IO.rope[IF note # NIL THEN AnnotationToRope[note] ELSE "none"]]; }; DropConv: PROC [msInfo: MsgSetInfo] ~ { filterID: ROPE; data: MsgData; msg: MsgHandle; subject, filterName, user: ROPE; query: TapFilter.Query; annot: TapFilter.Annotation; whichSimInterest: INT; oldInterestLevel: ROPE; dropSimInterest: ROPE; msg _ msInfo.selected; data _ NARROW[msg.data]; IF data.parsedMsg = NIL THEN { contents: ROPE _ BlackCherry.GetMsgContents[msInfo, msg].contents; data.parsedMsg _ TapFilter.ParseMsgIntoFields[contents]; }; subject _ LoganBerryEntry.GetAttr[entry: TapMsgQueue.EntryFromMsg[data.parsedMsg], type: $subject]; IF msInfo.selected = NIL THEN { BlackCherry.Report["\nNo message selected.\n"]; RETURN; }; oldInterestLevel _ NIL; filterID _ Rope.Cat[userName, "$", "Subject=", subject]; [filterName, user, query, annot] _ TapFilter.LookupFilter[filterDB: filterDBName, filterID: filterID]; IF filterName # NIL THEN { FOR anno: TapFilter.Annotation _ annot, anno.rest WHILE anno # NIL DO IF anno.first.type = $Level THEN oldInterestLevel _ anno.first.value; ENDLOOP; }; whichSimInterest _ PopUpSelection.Request[header: "Interest", choice: subMenuItems, headerDoc: NIL, choiceDoc: subMenuItemsDoc, default: 0, timeOut: 15]; IF whichSimInterest > 0 THEN { SELECT whichSimInterest FROM IN [1..19] => dropSimInterest _ ListNth[list: subMenuItems, itemNum: whichSimInterest]; = 20 => dropSimInterest _ defaultSimInterest; -- set default value = 21 => IF oldInterestLevel # NIL THEN dropSimInterest _ oldInterestLevel ELSE { BlackCherry.Report["\nNo original value because filter does not already exist.\n"]; RETURN }; ENDCASE; } ELSE RETURN; --user picked nothing, so abort entire operation IF oldInterestLevel # NIL THEN { IF Convert.IntFromRope[dropSimInterest] >= Convert.IntFromRope[oldInterestLevel] THEN { BlackCherry.Report["\nCannot drop interest level from %g to new level %g.\n", IO.rope[oldInterestLevel], IO.rope[dropSimInterest]]; RETURN; }; }; filterID _ DeleteSubjectFilter[msInfo: msInfo, msg: msInfo.selected]; BlackCherry.Report["\nDeleted old filter %g.", IO.rope[filterID]]; filterID _ AddSubjectFilter[msInfo: msInfo, msg: msInfo.selected, note: LIST[[$Level, dropSimInterest]]]; BlackCherry.Report["\nAdded new filter %g to set conversation's interest level to %g.\n", IO.rope[filterID], IO.rope[dropSimInterest]]; }; BoostConv: PROC [msInfo: MsgSetInfo] ~ { filterID: ROPE; data: MsgData; msg: MsgHandle; subject, filterName, user: ROPE; query: TapFilter.Query; annot: TapFilter.Annotation; whichSimInterest: INT; oldInterestLevel: ROPE; boostSimInterest, dropSimInterest: ROPE; msg _ msInfo.selected; data _ NARROW[msg.data]; IF data.parsedMsg = NIL THEN { contents: ROPE _ BlackCherry.GetMsgContents[msInfo, msg].contents; data.parsedMsg _ TapFilter.ParseMsgIntoFields[contents]; }; subject _ LoganBerryEntry.GetAttr[entry: TapMsgQueue.EntryFromMsg[data.parsedMsg], type: $subject]; IF msInfo.selected = NIL THEN { BlackCherry.Report["\nNo message selected.\n"]; RETURN; }; oldInterestLevel _ NIL; filterID _ Rope.Cat[userName, "$", "Subject=", subject]; [filterName, user, query, annot] _ TapFilter.LookupFilter[filterDB: filterDBName, filterID: filterID]; IF filterName # NIL THEN { FOR anno: TapFilter.Annotation _ annot, anno.rest WHILE anno # NIL DO IF anno.first.type = $Level THEN oldInterestLevel _ anno.first.value; ENDLOOP; }; whichSimInterest _ PopUpSelection.Request[header: "Interest", choice: subMenuItems, headerDoc: NIL, choiceDoc: subMenuItemsDoc, default: 0, timeOut: 15]; IF whichSimInterest > 0 THEN { SELECT whichSimInterest FROM IN [1..19] => boostSimInterest _ ListNth[list: subMenuItems, itemNum: whichSimInterest]; = 20 => boostSimInterest _ defaultSimInterest; -- set default value = 21 => IF oldInterestLevel # NIL THEN boostSimInterest _ oldInterestLevel ELSE { BlackCherry.Report["\nNo original value because filter does not already exist.\n"]; RETURN }; ENDCASE; } ELSE RETURN; --user picked nothing, so abort entire operation IF oldInterestLevel # NIL THEN { IF Convert.IntFromRope[boostSimInterest] <= Convert.IntFromRope[oldInterestLevel] THEN { BlackCherry.Report["\nCannot boost interest level from %g to new level %g.\n", IO.rope[oldInterestLevel], IO.rope[boostSimInterest]]; RETURN; }; }; filterID _ AddSubjectFilter[msInfo: msInfo, msg: msInfo.selected, note: LIST[[$Level, boostSimInterest]]]; BlackCherry.Report["\nAdded filter %g to set conversation's interest level to %g.\n", IO.rope[filterID], IO.rope[boostSimInterest]]; }; DropSim: PROC [msInfo: MsgSetInfo] ~ { filterID: ROPE; name: ROPE; filterName, user: ROPE; query: TapFilter.Query; annot: TapFilter.Annotation; oldInterestLevel, oldThreshold: ROPE; whichSimThreshold: INT; dropSimThreshold, dropSimInterest: ROPE; whichSimInterest: INT; IF msInfo.selected = NIL THEN { BlackCherry.Report["\nNo message selected.\n"]; RETURN; }; oldInterestLevel _ NIL; name _ BlackCherry.GetMsgID[msInfo: msInfo, msgH: msInfo.selected]; filterID _ Rope.Cat[userName, "$", "SimTo:", name]; [filterName, user, query, annot] _ TapFilter.LookupFilter[filterDB: filterDBName, filterID: filterID]; IF filterName # NIL THEN { FOR anno: TapFilter.Annotation _ annot, anno.rest WHILE anno # NIL DO IF anno.first.type = $SimThreshold THEN oldThreshold _ anno.first.value ELSE IF anno.first.type = $Level THEN oldInterestLevel _ anno.first.value; ENDLOOP; }; whichSimThreshold _ PopUpSelection.Request[header: "Sim Threshold", choice: subMenuItems, headerDoc: NIL, choiceDoc: subMenuItemsDoc, default: 0, timeOut: 15]; IF whichSimThreshold <= 0 THEN RETURN; -- no selection SELECT whichSimThreshold FROM IN [1..19] => dropSimThreshold _ ListNth[list: subMenuItems, itemNum: whichSimThreshold]; = 20 => dropSimThreshold _ defaultSimThreshold; --set default value = 21 => IF oldThreshold # NIL THEN dropSimThreshold _ oldThreshold ELSE { BlackCherry.Report["\nNo original value because filter does not exist.\n"]; RETURN }; ENDCASE; IF oldThreshold # NIL THEN { IF Convert.IntFromRope[dropSimThreshold] >= Convert.IntFromRope[oldThreshold] THEN { BlackCherry.Report["\n Cannot drop old similarity threshold %g to new, higher threshold %g.\n", IO.rope[oldThreshold], IO.rope[dropSimThreshold]]; RETURN; }; }; whichSimInterest _ PopUpSelection.Request[header: "Interest", choice: subMenuItems, headerDoc: NIL, choiceDoc: subMenuItemsDoc, default: 0, timeOut: 15]; IF whichSimInterest > 0 THEN { SELECT whichSimInterest FROM IN [1..19] => dropSimInterest _ ListNth[list: subMenuItems, itemNum: whichSimInterest]; = 20 => dropSimInterest _ defaultSimInterest; -- set default value = 21 => IF oldInterestLevel # NIL THEN dropSimInterest _ oldInterestLevel ELSE { BlackCherry.Report["\nNo original value because filter does not exist.\n"]; RETURN }; ENDCASE; } ELSE RETURN; BlackCherry.Report["\nReplacing old filter %g.\n", IO.rope[filterID]]; filterID _ DeleteTextFilter[msInfo: msInfo, msg: msInfo.selected]; filterID _ AddTextFilter[msInfo: msInfo, msg: msInfo.selected, note: LIST[[$Level, dropSimInterest], [$SimThreshold, dropSimThreshold]]]; BlackCherry.Report["\nAdding new filter %g to drop interest level of similar msgs to %g. Similarity threshold dropped to %g.\n", IO.rope[filterID], IO.rope[dropSimInterest], IO.rope[dropSimThreshold]]; }; BoostSim: PROC [msInfo: MsgSetInfo] ~ { filterID: ROPE; whichSimInterest, whichSimThreshold: INT; boostSimInterest, boostSimThreshold: ROPE; name: ROPE; oldThreshold, oldInterestLevel: ROPE; filterName, user: ROPE; query: TapFilter.Query; annot: TapFilter.Annotation; IF msInfo.selected = NIL THEN { BlackCherry.Report["\nNo message selected.\n"]; RETURN; }; oldThreshold _ NIL; oldInterestLevel _ NIL; name _ BlackCherry.GetMsgID[msInfo: msInfo, msgH: msInfo.selected]; filterID _ Rope.Cat[userName, "$", "SimTo:", name]; [filterName, user, query, annot] _ TapFilter.LookupFilter[filterDB: filterDBName, filterID: filterID]; IF filterName # NIL THEN { FOR anno: TapFilter.Annotation _ annot, anno.rest WHILE anno # NIL DO IF anno.first.type = $SimThreshold THEN oldThreshold _ anno.first.value ELSE IF anno.first.type = $Level THEN oldInterestLevel _ anno.first.value; ENDLOOP; }; whichSimThreshold _ PopUpSelection.Request[header: "Threshold", choice: subMenuItems, headerDoc: NIL, choiceDoc: subMenuItemsDoc, default: 0, timeOut: 15]; IF whichSimThreshold > 0 THEN { SELECT whichSimThreshold FROM IN [1..19] => boostSimThreshold _ ListNth[list: subMenuItems, itemNum: whichSimThreshold]; = 20 => boostSimThreshold _ defaultSimThreshold; --set default value = 21 => IF oldThreshold # NIL THEN boostSimThreshold _ oldThreshold ELSE { BlackCherry.Report["\nNo original value because filter does not exist.\n"]; RETURN }; ENDCASE; } ELSE RETURN; --user picked nothing, so abort entire operation IF oldThreshold # NIL THEN { IF Convert.IntFromRope[boostSimThreshold] <= Convert.IntFromRope[oldThreshold] THEN { BlackCherry.Report["\nCannot boost old similarity threshold %g to new, lower threshold %g.\n", IO.rope[oldThreshold], IO.rope[boostSimThreshold]]; RETURN; }; }; whichSimInterest _ PopUpSelection.Request[header: "Interest", choice: subMenuItems, headerDoc: NIL, choiceDoc: subMenuItemsDoc, default: 0, timeOut: 15]; IF whichSimInterest > 0 THEN { SELECT whichSimInterest FROM IN [1..19] => boostSimInterest _ ListNth[list: subMenuItems, itemNum: whichSimInterest]; = 20 => boostSimInterest _ defaultSimInterest; -- set default value = 21 => IF oldInterestLevel # NIL THEN boostSimInterest _ oldInterestLevel ELSE { BlackCherry.Report["\nnNo original value because filter does not exist.\n"]; RETURN }; ENDCASE; } ELSE RETURN; filterID _ AddTextFilter[msInfo: msInfo, msg: msInfo.selected, note: LIST[[$Level, boostSimInterest], [$SimThreshold, boostSimThreshold]]]; BlackCherry.Report["\nAdded filter %g to boost interest level of similar msgs to %g. Similarity threshold boosted to %g.\n", IO.rope[filterID], IO.rope[boostSimInterest], IO.rope[boostSimThreshold]]; }; AddSubjectFilter: PROC [msInfo: MsgSetInfo, msg: MsgHandle, note: TapFilter.Annotation] RETURNS [filterID: ROPE] ~ { subject, query: ROPE; data: MsgData _ NARROW[msg.data]; IF data.parsedMsg = NIL THEN { contents: ROPE _ BlackCherry.GetMsgContents[msInfo, msg].contents; data.parsedMsg _ TapFilter.ParseMsgIntoFields[contents]; }; subject _ LoganBerryEntry.GetAttr[entry: TapMsgQueue.EntryFromMsg[data.parsedMsg], type: $subject]; IF Rope.Find[s1: subject, s2: "Re: ", pos1: 0, case: FALSE] = 0 THEN subject _ Rope.Substr[base: subject, start: 4]; -- strip off "re: " query _ IO.PutFR["subject(re): \"(Re\':| )*%g\"", IO.rope[subject]]; GetProfileInfo[]; filterID _ Rope.Cat[userName, "$", "Subject=", subject]; IF TapFilter.ExistsFilter[filterDB: filterDBName, filterID: filterID] THEN { [] _ TapFilter.DeleteFilter[filterDB: filterDBName, filterID: filterID]; }; filterID _ TapFilter.AddFilter[filterDB: filterDBName, user: userName, filterName: Rope.Concat["Subject=", subject], query: query, annot: note, agent: filteringAgent]; }; DeleteSubjectFilter: PROC [msInfo: MsgSetInfo, msg: MsgHandle] RETURNS [filterID: ROPE] ~ { subject, query: ROPE; data: MsgData _ NARROW[msg.data]; IF data.parsedMsg = NIL THEN { contents: ROPE _ BlackCherry.GetMsgContents[msInfo, msg].contents; data.parsedMsg _ TapFilter.ParseMsgIntoFields[contents]; }; subject _ LoganBerryEntry.GetAttr[entry: TapMsgQueue.EntryFromMsg[data.parsedMsg], type: $subject]; IF Rope.Find[s1: subject, s2: "Re: ", pos1: 0, case: FALSE] = 0 THEN subject _ Rope.Substr[base: subject, start: 4]; -- strip off "re: " query _ IO.PutFR["subject(re): \"(Re\':| )*%g\"", IO.rope[subject]]; GetProfileInfo[]; filterID _ Rope.Cat[userName, "$", "Subject=", subject]; TapFilter.DeleteFilter[filterDB: filterDBName, filterID: filterID]; }; AddTextFilter: PROC [msInfo: MsgSetInfo, msg: MsgHandle, note: TapFilter.Annotation] RETURNS [filterID: ROPE] ~ { name, query, text: ROPE; attrs: LIST OF ROPE; stream: IO.STREAM; threshold: ROPE; data: MsgData _ NARROW[msg.data]; aps: LoganQuery.AttributePatterns; ap: LoganQuery.AttributePattern _ NEW[LoganQuery.AttributePatternRec]; ap.attr.type _ $text; ap.ptype _ IO.PutFR["sim"]; IF data.parsedMsg = NIL THEN { contents: ROPE _ BlackCherry.GetMsgContents[msInfo, msg].contents; data.parsedMsg _ TapFilter.ParseMsgIntoFields[contents]; }; attrs _ LoganBerryEntry.GetAllAttrs[entry: TapMsgQueue.EntryFromMsg[data.parsedMsg], type: $text]; WHILE attrs # NIL DO text _ Rope.Concat[text, attrs.first]; attrs _ attrs.rest; ENDLOOP; stream _ IO.RIS[text]; stream _ SimMatch.Tokenize[stream]; text _ IO.RopeFromROS[stream]; FOR anno: TapFilter.Annotation _ note, anno.rest WHILE anno # NIL DO IF anno.first.type = $SimThreshold THEN threshold _ anno.first.value; ENDLOOP; text _ Rope.Concat[threshold, text]; -- Prepend the threshold ap.attr.value _ text; aps _ LIST[ap]; stream _ IO.ROS[]; LoganQuery.WriteAttributePatterns[s: stream, ap: aps]; query _ IO.RopeFromROS[stream]; name _ BlackCherry.GetMsgID[msInfo: msInfo, msgH: msg]; GetProfileInfo[]; IF TapFilter.ExistsFilter[filterDB: filterDBName, filterID: Rope.Cat[userName, "$", "SimTo:", name]] THEN { BlackCherry.Report["\Replacing old filter %g.\n", IO.rope[filterID]]; [] _ DeleteTextFilter[msInfo, msg]; }; filterID _ TapFilter.AddFilter[filterDB: filterDBName, user: userName, filterName: Rope.Concat["SimTo:", name], query: query, annot: note, agent: filteringAgent]; }; DeleteTextFilter: PROC [msInfo: MsgSetInfo, msg: MsgHandle] RETURNS [filterID: ROPE] ~ { name: ROPE; stream: IO.STREAM; name _ BlackCherry.GetMsgID[msInfo: msInfo, msgH: msg]; GetProfileInfo[]; filterID _ Rope.Cat[userName, "$", "SimTo:", name]; TapFilter.DeleteFilter[filterDB: filterDBName, filterID: filterID]; }; ListNth: PROC [list: LIST OF ROPE, itemNum: INT] RETURNS [nth: ROPE] ~ { item: ROPE; counter: INT _ 0; FOR element: LIST OF ROPE _ list, element.rest WHILE element # NIL DO counter _ counter + 1; item _ element.first; IF counter = itemNum THEN RETURN[item]; ENDLOOP; }; GetProfileInfo: PROC [] ~ { IF checkProfile THEN { userName _ UserProfile.Token[key: "Tapestry.UserName", default: userName]; filterDBName _ UserProfile.Token[key: "Tapestry.FilterDB", default: filterDBName]; annotationDBName _ UserProfile.Token[key: "Tapestry.AnnotationDB", default: annotationDBName]; checkProfile _ FALSE; }; }; ProfileChanged: UserProfile.ProfileChangedProc = { checkProfile _ TRUE; }; CustomizeBlackCherry: PROC ~ { BlackCherry.RegisterCustomProcs[procs: tapProcs]; BlackCherry.AddDisplayerProc[menuName: "Filters", proc: FiltersMenuProc]; UserProfile.CallWhenProfileChanges[proc: ProfileChanged]; }; CustomizeBlackCherry[]; END. ΈTapInBlackCherry.mesa Copyright Σ 1990 by Xerox Corporation. All rights reserved. Doug Terry, July 5, 1990 10:07:15 am PDT Theimer, April 24, 1990 11:09 am PDT Simply registers procedures with BlackCherry; does not export any code. Sabel, August 15, 1990 8:40 pm PDT Brian Oki, April 4, 1991 4:29 pm PST A hack to distinquish between old and new mail: When BlackCherry calls the InsertMsgsProc, it does not state whether the group of messages are new mail or old mail. We want to know since these are handled differently, i.e. old mail messages are sorted together whereas new mail is kept separately. For new mail, the ProcessNewMailProc is always called before the InsertMsgsProc. So, ProcessNewMailProc sets the newMail flag to TRUE and InsertMsgsProc resets it to FALSE. BlackCherry customization procedures PROC [msInfo: MsgSetInfo, msgH: MsgHandle] RETURNS [text: ROPE] PROC [msInfo: MsgSetInfo, msgH: MsgHandle] RETURNS [] Sort messages in interest order; new mail messages are appended to the message set while old mail is inserted into the message set in sorted order. PROC [msInfo: MsgSetInfo, msgH: MsgHandle] RETURNS [] Create filtering agent if necessary. Parse msgs and place on filter queue. Wakeup agent to filter messages and wait until it is finished. [msgID: ROPE, msg: TapMsgQueue.Msg, filterID: ROPE, annot: TapFilter.Annotation] RETURNS [doIt: BOOLEAN _ TRUE] A message's ilevel is cached in the msgHandle. If not there, lookup the msg's ilevel in the annotation database. The default ilevel is used if a database entry is not found for this message. If the message is annotated with several ilevels then the highest one is used. Takes a list of messages that are unsorted and a list that is already sorted; returns a sorted list containing both sets of messages. The returned messages are sorted by interest level. The sort is destructive. Currently, a simple insertion sort is used. Remove message from head of unsorted list and insert into sorted list. Places msg on list of msgs in ilevel order. Assumes that the msg list is already sorted. Note: this is a dumb algorithm that could be improved; calling InsertMsg repeatly yields an insertion sort. Menu operations [parent: ViewerClasses.Viewer, clientData: REF ANY _ NIL, mouseButton: ViewerClasses.MouseButton _ red, shift: BOOL _ FALSE, control: BOOL _ FALSE] Get subject Construct filterID, lookup filter in database, and extract annotation of interest level. Assign old interest level. User picks the interest level for conversation messages. No need to check old interest level against new interest level. Check to see whether user's supplied interest is greater than the old one; if so, then we're done, else, report error. Construct filterID, lookup filter in database, and extract annotation of interest level. Assign old interest level. User picks the interest level for conversation messages. No need to check old interest level against new interest level. Check to see whether user's supplied interest is less than the old one; if so, then we're done, else, report error. Effects: Drops the similarity threshold of a filter to a value less than or equal to the original value. Used to set both the interest level and the similarity threshold for the selected message. User must choose values for both, using default values if necessary; otherwise, this operation has no effect, that is, the old filter is not changed or the new filter is not added. Clicking outside the menu causes the operation to return. User can select a value between 5 and 95 inclusive, or choose "default" or "original" values. Pre-condition: filter must already exist. Construct filterID, lookup filter in database, and extract annotation of similarity threshold. Assign old interest level and old similarity threshold. User picks the new threshold for similarity matching, above which he's interested in. Check to see whether user's supplied threshold is greater than the old one; if so, then we're done, else, report error. User picks the interest level for similar messages. No need to check old interest level against new interest level. Effects: This routine boosts the similarity threshold for a selected message to a higher value, as determinted by the user, and changes the interest level for similar messages. User must choose values for both, using default values if necessary; otherwise, this operation has no effect, that is, the old filter is not changed or the new filter is not added. Clicking outside the menu causes the operation to return. User can select a value between 5 and 95 inclusive, or choose "default" or "original" values. Construct filterID, lookup filter in database, and extract annotation of similarity threshold. Assign old interest level and old similarity threshold. User picks the threshold for similarity matching, above which he's interested in. Check to see whether user's supplied threshold is less than or equal to the old one; if so, then we're done, else, report error. User picks the interest level for similar messages. No need to check old interest level against new interest level. Add a text filter for similarity matching. Get all text fields and concatenate into one rope Tokenize text (remove punctuation, etc.) SimMatch.UpdateDFList[text]; Get threshold value from annotation. BlackCherry.Report["query is %g\n", IO.rope[query]]; -- for debugging Effects: This procedure returns the nth element in a list of ropes. Per-user profile information [reason: UserProfile.ProfileChangeReason] Registration Κ¬– "cedar" style•NewlineDelimiter ˜šœ™Icode™Kšœ&˜&Kšœ8œ˜>Kšœ ˜ Kšœ œ˜K˜K™—–s -- [msgID: ROPE, msg: TapMsgQueue.Msg, filterID: ROPE, annot: TapFilter.Annotation] RETURNS [doIt: BOOLEAN _ TRUE]š‘œ˜)KšΠcko™oKšœ˜K˜—K˜š‘ œœœ œ˜>K™šœ˜KšœCœ œ˜gKšœ˜ Kšœ˜—š‘œœ œœœœœ˜7K˜š œœœœœœ˜8Kšœœ!˜'šœ ˜K˜—Kšœ˜—K˜—Kšœ˜šœ œ˜Kšœ œ ˜—Kšœ œ ˜šœœŸ4˜RKšœ˜šœ œ˜Kšœ4˜4—K–T[conv: LoganBerry.Conv _ NIL, db: LoganBerry.OpenDB, key: ATOM, value: ROPE]˜KšœN˜NKš œœ œœœ>˜uK˜—Kšœ˜K˜K˜—š‘œœœ˜QK™šœ œ˜K™FKšœ˜K˜Jšœ ˜ Kšœ˜—Kšœ ˜K˜K˜—š‘ œœ%œ˜PKšœΖ™ΖKšœœ˜Kšœœ˜ Kšœ œ˜Kšœ œœœ˜"Kšœ ˜ šœ%œœ˜;šœœŸ˜CK˜šœ˜ Kšœ ˜Kšœ˜—Kšœ˜K˜—Kšœ ˜ Kšœ˜—šœ œœŸ˜-K˜—K˜——™Kš œ œœœœC˜aš œœœœœ˜"Kšœ,˜,Kšœ3˜3Kšœ4˜4Kšœ,˜,Kšœ-˜-Kšœ˜—K˜Kš œœœœœ‰˜ͺš œœœœœL˜pKšœ3˜3—K˜Kšœ œ˜Kšœ œ˜Kšœœ˜ Kšœœ˜!K˜š‘œ˜#Kš’“™“šœ˜Kšœœ˜KšœFœ œœ˜uK˜—Kšœœ˜Kšœœ˜Kšœœœ˜OKšœœKœ4˜ŒKšœ œœŸ˜+Kšœ˜Kšœ˜K˜šœ˜Kšœ˜Kšœ˜Kšœ˜Kšœ˜Kšœ˜Kšœ˜—K˜K˜—š‘œœœ œ˜QK–ldStream: STREAM _ NIL]šœœ˜ šœ(œœ˜;K–―[stream: STREAM, format: ROPE _ NIL, v1: IO.Value _ [null[]], v2: IO.Value _ [null[]], v3: IO.Value _ [null[]], v4: IO.Value _ [null[]], v5: IO.Value _ [null[]]]š œœœ œ$œ˜nKšœ˜—K˜K˜—š‘ œœ˜,Kšœ˜šœœœ˜Kšœ/˜/Kšœ˜K˜—K˜Kšœ5œ˜TK– [annotDB: ROPE, msgID: ROPE]šœX˜XKš œœœœœœ ˜[K˜K˜—š‘œœ˜'Kšœ œ˜Kšœ˜Kšœ˜Kšœœ˜ Kšœ˜Kšœ˜Kšœœ˜Kšœœ˜Kšœœ˜K˜Kšœ˜Kšœœ ˜šœœœ˜Kšœ œ4˜BKšœ8˜8K˜—K™ K–)[entry: LoganBerry.Entry, type: ATOM]šœc˜cK˜šœœœ˜Kšœ/˜/Kšœ˜K˜K˜—Kšœœ˜K™XKšœ9˜9Kšœh˜hšœœœ˜K™šœ/œœ˜EKšœœ%˜EKšœ˜—K˜K˜—K™yKšœ_œ7˜™šœœ˜šœ˜KšœU˜WKšœ/Ÿ˜Cšœœœœ#˜Išœ˜KšœS˜SKš˜K˜——Kšœ˜—K˜KšœœŸ0˜=—K˜K™wšœœœ˜ šœPœ˜XKšœNœœ˜ƒKšœ˜K˜—Kšœ˜—K˜KšœE˜EKšœ/œ˜BKšœHœ˜iKšœZœ+˜‡K˜K˜—š‘ œœ˜(Kšœ œ˜Kšœ˜Kšœ˜Kšœœ˜ Kšœ˜Kšœ˜Kšœœ˜Kšœœ˜Kšœ#œ˜(K˜Kšœ˜Kšœœ ˜šœœœ˜Kšœ œ4˜BKšœ8˜8K˜—K–)[entry: LoganBerry.Entry, type: ATOM]šœc˜cK˜šœœœ˜Kšœ/˜/Kšœ˜K˜—Kšœœ˜K™XKšœ9˜9Kšœh˜hšœœœ˜K™šœ/œœ˜EKšœœ%˜EKšœ˜—K˜K˜—K™yKšœ_œ7˜™šœœ˜šœ˜KšœV˜XKšœ0Ÿ˜Dšœœœœ$˜Jšœ˜KšœS˜SKš˜K˜——Kšœ˜—K˜KšœœŸ0˜=—K˜K™tšœœœ˜ šœPœ˜XKšœOœœ˜…Kšœ˜K˜—Kšœ˜—K˜KšœHœ˜jKšœVœœ˜„K˜K˜—š‘œœ˜&K™•Kšœ œ˜Kšœœ˜ Kšœœ˜Kšœ˜Kšœ˜Kšœ œ˜%Kšœœ˜Kšœ#œ˜(Kšœœ˜K˜šœœœ˜Kšœ/˜/Kšœ˜K˜K˜—K™*Kšœœ˜K˜K˜CK™^Kšœ4˜4Kšœh˜hšœœœ˜K™7šœ/œœ˜Ešœ!œ ˜GKšœœœ%˜J—Kšœ˜—K˜—K˜K™UKšœeœ7˜ŸKšœœœŸ˜7šœ˜KšœW˜YKšœ0Ÿ˜Cšœœœœ ˜Bšœ˜KšœK˜KKš˜K˜——Kšœ˜K˜—K™yšœœœ˜šœMœcœœ˜θKšœ˜K˜—K˜K˜—K™tKšœ_œ7˜™šœœ˜šœ˜KšœU˜WKšœ/Ÿ˜Cšœœœœ#˜Išœ˜KšœK˜KKš˜K˜——Kšœ˜—K˜Kšœœ˜ —˜K˜—Kšœ3œ˜FKšœB˜BKšœEœ@˜‰Kšœœœœ˜ΙK˜—K˜š‘œœ˜'K™K˜Kšœ œ˜Kšœ%œ˜)Kšœ%œ˜*Kšœœ˜ Kšœ œ˜%Kšœœ˜Kšœ˜Kšœ˜K˜šœœœ˜Kšœ/˜/Kšœ˜K˜—Kšœœ˜šœœ˜K˜—K˜CK™^Kšœ4˜4Kšœh˜hšœœœ˜K™7šœ/œœ˜Ešœ!œ ˜GKšœœœ%˜J—Kšœ˜—K˜—K˜K™QKšœaœ7˜›šœœ˜šœ˜KšœX˜ZKšœ1Ÿ˜Dšœœœœ!˜Cšœ˜KšœK˜KKš˜K˜——Kšœ˜—Kšœ˜KšœœŸ0˜>—K˜K™šœœœ˜šœNœ˜VKšœ_œœ˜’Kšœ˜K˜—Kšœ˜—K˜K™tKšœ_œ7˜™šœœ˜šœ˜KšœV˜XKšœ0Ÿ˜Dšœœœœ$˜Jšœ˜KšœL˜LKš˜K˜——Kšœ˜—K˜Kšœœ˜ —K˜KšœEœB˜‹Kšœ}œœœ˜ΗK˜—˜K˜—š‘œœBœ œ˜tKšœœ˜Kšœœ ˜!šœœœ˜Kšœ œ4˜BKšœ8˜8K˜—K–)[entry: LoganBerry.Entry, type: ATOM]šœc˜c–>[s1: ROPE, s2: ROPE, pos1: INT _ 0, case: BOOL _ TRUE]šœ3œ˜DK–9[base: ROPE, start: INT _ 0, len: INT _ 2147483647]šœ1Ÿ˜D—Kšœœ(œ˜DK˜Kšœ9˜9šœDœ˜LKšœH˜HKšœ˜—Kšœ§˜§K˜K˜—š‘œœ&œ œ˜[Kšœœ˜Kšœœ ˜!šœœœ˜Kšœ œ4˜BKšœ8˜8K˜—K–)[entry: LoganBerry.Entry, type: ATOM]šœc˜c–>[s1: ROPE, s2: ROPE, pos1: INT _ 0, case: BOOL _ TRUE]šœ3œ˜DK–9[base: ROPE, start: INT _ 0, len: INT _ 2147483647]šœ1Ÿ˜D—Kšœœ(œ˜DK˜Kšœ9˜9KšœC˜CK˜K˜—K˜Kšœ*™*š‘ œœBœ œ˜qKšœœ˜Kšœœœœ˜Kšœœœ˜Kšœ œ˜K˜Kšœœ ˜!K˜"Kšœ"œ!˜FKšœ˜Kšœ œ˜K˜šœœœ˜Kšœ œ4˜BKšœ8˜8K˜K˜—Kšœ1™1Kšœb˜bšœ œ˜K˜&K˜Kšœ˜—K™K™(Kšœ œœ˜K˜#Kšœœ˜K™K™$šœ.œœ˜DKšœ!œ˜EKšœ˜—Kšœ%Ÿ˜=K˜Kšœ˜Kšœœ˜Kšœ œœ˜Kšœ6˜6Kšœœ˜K˜Kšœ$œŸ™EK˜K˜7K˜šœdœ˜lKšœ2œ˜EKšœ#˜#Kšœ˜—Kšœ’˜’K˜K˜—š‘œœ&œ œ˜XKšœœ˜ Kšœœœ˜K˜K˜7K˜Kšœ4˜4KšœC˜CK˜—K˜š‘œœœœœ œœœ˜HK™DKšœœ˜ Kšœ œ˜š œ œœœœ œ˜EK˜K˜Kšœœœ˜'Kšœ˜—K˜K˜——™š‘œœ˜–‚[filterDB: ROPE, user: ROPE, filterName: ROPE, query: ROPE, annot: TapFilter.Annotation, agent: TapFilter.Agent _ NIL]šœ˜K–$[key: ROPE, default: ROPE _ NIL]šœJ˜JK–$[key: ROPE, default: ROPE _ NIL]šœR˜RK–$[key: ROPE, default: ROPE _ NIL]šœ^˜^Kšœœ˜K˜—K˜K˜—–- -- [reason: UserProfile.ProfileChangeReason]š‘œ$˜2Kš’)™)Kšœœ˜K˜——šœ ™ š‘œœ˜K˜1KšœI˜IK–([proc: UserProfile.ProfileChangedProc]˜9Kšœ˜K˜—Kšœ˜—K˜šœ˜K˜—K˜—…—W’‰φ