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
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: BOOLFALSE;
userName: ROPENIL;
filterDBName: ROPE ← "TapFiltersLB.df";
annotationDBName: ROPE ← "TapAnnotationsLB.df";
checkProfile: BOOLEANTRUE;
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: BOOLEANFALSE;
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
tapProcs: BlackCherry.CustomProcs ← NEW[BlackCherry.CustomProcsRec ← [newMail: FilterMessages, insertMsgs: AddInInterestOrder, msgButtonText: TOCWithInterestLevel]];
TOCWithInterestLevel: BlackCherry.MsgButtonTextProc ~ {
PROC [msInfo: MsgSetInfo, msgH: MsgHandle] RETURNS [text: ROPE]
text ← IO.PutFR["%3g %g", IO.int[GetMsgILevel[msgH]], IO.rope[msgH.toc]];
};
AddInInterestOrder: BlackCherry.InsertMsgsProc ~ {
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.
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 ~ {
PROC [msInfo: MsgSetInfo, msgH: MsgHandle] RETURNS []
ENABLE TapFilter.Error => {
BlackCherry.Report["Problem with filtering agent: %g - %g.\n", IO.atom[ec], IO.rope[explanation]];
CONTINUE;
};
Create filtering agent if necessary.
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];
};
Parse msgs and place on filter queue.
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;
Wakeup agent to filter messages and wait until it is finished.
TapFilter.WakeupAgent[filteringAgent];
[] ← TapFilter.IsAgentIdle[agent: filteringAgent, wait: TRUE];
BlackCherry.Report[" done.\n"];
newMail ← TRUE;
};
ReportProgress: TapFilter.MonitorProc = {
[msgID: ROPE, msg: TapMsgQueue.Msg, filterID: ROPE, annot: TapFilter.Annotation] RETURNS [doIt: BOOLEAN ← TRUE]
BlackCherry.Report["@"];
};
GetMsgILevel: PROC [msgH: MsgHandle] RETURNS [ilevel: INT] ~ {
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.
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] ~ {
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.
WHILE unsorted # NIL DO
Remove message from head of unsorted list and insert into sorted list.
msg: MsgHandle ← unsorted;
unsorted ← unsorted.next;
sorted ← InsertMsg[msg, sorted];
ENDLOOP;
RETURN[sorted];
};
InsertMsg: PROC [msg: MsgHandle, sorted: MsgHandle] RETURNS [new: MsgHandle] ~ {
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.
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;
};
Menu operations
menuItems: LIST OF ROPELIST["InterestLevel?", "DropConv", "BoostConv", "DropSim", "BoostSim"];
menuItemsDoc: LIST OF ROPELIST[
"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 ROPELIST["5", "10", "15", "20", "25", "30", "35", "40", "45",
"50", "55", "60", "65", "70", "75", "80", "85", "90", "95", "default", "original"];
subMenuItemsDoc: LIST OF ROPELIST["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
"Set to default value?", "Set to original value?"];
dropLevel: ROPE ← "25";
boostLevel: ROPE ← "75";
defaultSimInterest: ROPE ← "50";
defaultSimThreshold: ROPE ← "50";
FiltersMenuProc: Menus.MenuProc ~ {
[parent: ViewerClasses.Viewer, clientData: REF ANY ← NIL, mouseButton: ViewerClasses.MouseButton ← red, shift: BOOL ← FALSE, control: BOOL ← FALSE]
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];
};
Get subject
subject ← LoganBerryEntry.GetAttr[entry: TapMsgQueue.EntryFromMsg[data.parsedMsg], type: $subject];
IF msInfo.selected = NIL THEN {
BlackCherry.Report["\nNo message selected.\n"];
RETURN;
};
oldInterestLevel ← NIL;
Construct filterID, lookup filter in database, and extract annotation of interest level.
filterID ← Rope.Cat[userName, "$", "Subject=", subject];
[filterName, user, query, annot] ← TapFilter.LookupFilter[filterDB: filterDBName, filterID: filterID];
IF filterName # NIL THEN {
Assign old interest level.
FOR anno: TapFilter.Annotation ← annot, anno.rest WHILE anno # NIL DO
IF anno.first.type = $Level THEN oldInterestLevel ← anno.first.value;
ENDLOOP;
};
User picks the interest level for conversation messages. No need to check old interest level against new interest level.
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
Check to see whether user's supplied interest is greater than the old one; if so, then we're done, else, report error.
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;
Construct filterID, lookup filter in database, and extract annotation of interest level.
filterID ← Rope.Cat[userName, "$", "Subject=", subject];
[filterName, user, query, annot] ← TapFilter.LookupFilter[filterDB: filterDBName, filterID: filterID];
IF filterName # NIL THEN {
Assign old interest level.
FOR anno: TapFilter.Annotation ← annot, anno.rest WHILE anno # NIL DO
IF anno.first.type = $Level THEN oldInterestLevel ← anno.first.value;
ENDLOOP;
};
User picks the interest level for conversation messages. No need to check old interest level against new interest level.
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
Check to see whether user's supplied interest is less than the old one; if so, then we're done, else, report error.
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] ~ {
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.
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;
};
Pre-condition: filter must already exist.
oldInterestLevel ← NIL;
name ← BlackCherry.GetMsgID[msInfo: msInfo, msgH: msInfo.selected];
Construct filterID, lookup filter in database, and extract annotation of similarity threshold.
filterID ← Rope.Cat[userName, "$", "SimTo:", name];
[filterName, user, query, annot] ← TapFilter.LookupFilter[filterDB: filterDBName, filterID: filterID];
IF filterName # NIL THEN {
Assign old interest level and old similarity threshold.
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;
};
User picks the new threshold for similarity matching, above which he's interested in.
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;
Check to see whether user's supplied threshold is greater than the old one; if so, then we're done, else, report error.
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;
};
};
User picks the interest level for similar messages. No need to check old interest level against new interest level.
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] ~ {
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.
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];
Construct filterID, lookup filter in database, and extract annotation of similarity threshold.
filterID ← Rope.Cat[userName, "$", "SimTo:", name];
[filterName, user, query, annot] ← TapFilter.LookupFilter[filterDB: filterDBName, filterID: filterID];
IF filterName # NIL THEN {
Assign old interest level and old similarity threshold.
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;
};
User picks the threshold for similarity matching, above which he's interested in.
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
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.
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;
};
};
User picks the interest level for similar messages. No need to check old interest level against new interest level.
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];
};
Add a text filter for similarity matching.
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];
};
Get all text fields and concatenate into one rope
attrs ← LoganBerryEntry.GetAllAttrs[entry: TapMsgQueue.EntryFromMsg[data.parsedMsg], type: $text];
WHILE attrs # NIL DO
text ← Rope.Concat[text, attrs.first];
attrs ← attrs.rest;
ENDLOOP;
Tokenize text (remove punctuation, etc.)
stream ← IO.RIS[text];
stream ← SimMatch.Tokenize[stream];
text ← IO.RopeFromROS[stream];
SimMatch.UpdateDFList[text];
Get threshold value from annotation.
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];
BlackCherry.Report["query is %g\n", IO.rope[query]]; -- for debugging
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] ~ {
Effects: This procedure returns the nth element in a list of ropes.
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;
};
Per-user profile information
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 = {
[reason: UserProfile.ProfileChangeReason]
checkProfile ← TRUE;
};
Registration
CustomizeBlackCherry: PROC ~ {
BlackCherry.RegisterCustomProcs[procs: tapProcs];
BlackCherry.AddDisplayerProc[menuName: "Filters", proc: FiltersMenuProc];
UserProfile.CallWhenProfileChanges[proc: ProfileChanged];
};
CustomizeBlackCherry[];
END.