Anupam K
5 years ago
19 changed files with 885 additions and 2 deletions
@ -0,0 +1,66 @@ |
|||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
|||
// For license information, please see license.txt
|
|||
|
|||
frappe.ui.form.on('LinkedIn Settings', { |
|||
onload: function(frm){ |
|||
if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ |
|||
frappe.confirm( |
|||
'Session not valid, Do you want to login?', |
|||
function(){ |
|||
frm.trigger("login"); |
|||
}, |
|||
function(){ |
|||
window.close(); |
|||
} |
|||
) |
|||
} |
|||
}, |
|||
refresh: function(frm){ |
|||
if(frm.doc.session_status=="Expired"){ |
|||
frm.dashboard.set_headline_alert( |
|||
'<div class="row">' + |
|||
'<div class="col-xs-12">' + |
|||
'<span class="indicator whitespace-nowrap red'+ '' +'"><span class="hidden-xs">Session Not Active. Save doc to login.</span></span> ' + |
|||
'</div>' + |
|||
'</div>' |
|||
); |
|||
} |
|||
if(frm.doc.session_status=="Active"){ |
|||
let d = new Date(frm.doc.modified) |
|||
d.setDate(d.getDate()+60); |
|||
let dn = new Date() |
|||
let days = d.getTime() - dn.getTime(); |
|||
days = Math.floor(days/(1000 * 3600 * 24)); |
|||
let msg,color; |
|||
if(days>0){ |
|||
msg = "Your Session will be expire in " + days + " days."; |
|||
color = "green"; |
|||
} |
|||
else{ |
|||
msg = "Session is expired. Save doc to login."; |
|||
color = "red"; |
|||
} |
|||
frm.dashboard.set_headline_alert( |
|||
'<div class="row">' + |
|||
'<div class="col-xs-12">' + |
|||
'<span class="indicator whitespace-nowrap '+ color +'"><span class="hidden-xs">' + msg + ' </span></span> ' + |
|||
'</div>' + |
|||
'</div>' |
|||
); |
|||
} |
|||
}, |
|||
login: function(frm){ |
|||
if(frm.doc.consumer_key && frm.doc.consumer_secret){ |
|||
frappe.call({ |
|||
doc: frm.doc, |
|||
method: "get_authorization_url", |
|||
callback : function(r) { |
|||
window.location.href = r.message; |
|||
} |
|||
}); |
|||
} |
|||
}, |
|||
after_save: function(frm){ |
|||
frm.trigger("login"); |
|||
} |
|||
}); |
@ -0,0 +1,111 @@ |
|||
{ |
|||
"actions": [], |
|||
"creation": "2020-01-30 13:36:39.492931", |
|||
"doctype": "DocType", |
|||
"editable_grid": 1, |
|||
"engine": "InnoDB", |
|||
"field_order": [ |
|||
"account_name", |
|||
"column_break_2", |
|||
"company_id", |
|||
"oauth_details", |
|||
"consumer_key", |
|||
"column_break_5", |
|||
"consumer_secret", |
|||
"user_details_section", |
|||
"access_token", |
|||
"person_urn", |
|||
"session_status" |
|||
], |
|||
"fields": [ |
|||
{ |
|||
"fieldname": "account_name", |
|||
"fieldtype": "Data", |
|||
"label": "Account Name", |
|||
"read_only": 1 |
|||
}, |
|||
{ |
|||
"fieldname": "oauth_details", |
|||
"fieldtype": "Section Break", |
|||
"label": "OAuth Credentials" |
|||
}, |
|||
{ |
|||
"fieldname": "consumer_key", |
|||
"fieldtype": "Data", |
|||
"in_list_view": 1, |
|||
"label": "Consumer Key", |
|||
"reqd": 1 |
|||
}, |
|||
{ |
|||
"fieldname": "consumer_secret", |
|||
"fieldtype": "Password", |
|||
"in_list_view": 1, |
|||
"label": "Consumer Secret", |
|||
"reqd": 1 |
|||
}, |
|||
{ |
|||
"fieldname": "access_token", |
|||
"fieldtype": "Data", |
|||
"hidden": 1, |
|||
"label": "Access Token", |
|||
"read_only": 1 |
|||
}, |
|||
{ |
|||
"fieldname": "person_urn", |
|||
"fieldtype": "Data", |
|||
"hidden": 1, |
|||
"label": "Person URN", |
|||
"read_only": 1 |
|||
}, |
|||
{ |
|||
"fieldname": "column_break_5", |
|||
"fieldtype": "Column Break" |
|||
}, |
|||
{ |
|||
"fieldname": "user_details_section", |
|||
"fieldtype": "Section Break", |
|||
"label": "User Details" |
|||
}, |
|||
{ |
|||
"fieldname": "session_status", |
|||
"fieldtype": "Select", |
|||
"hidden": 1, |
|||
"label": "Session Status", |
|||
"options": "Expired\nActive", |
|||
"read_only": 1 |
|||
}, |
|||
{ |
|||
"fieldname": "column_break_2", |
|||
"fieldtype": "Column Break" |
|||
}, |
|||
{ |
|||
"fieldname": "company_id", |
|||
"fieldtype": "Data", |
|||
"label": "Company ID", |
|||
"reqd": 1 |
|||
} |
|||
], |
|||
"issingle": 1, |
|||
"links": [], |
|||
"modified": "2020-04-16 23:22:51.966397", |
|||
"modified_by": "Administrator", |
|||
"module": "CRM", |
|||
"name": "LinkedIn Settings", |
|||
"owner": "Administrator", |
|||
"permissions": [ |
|||
{ |
|||
"create": 1, |
|||
"delete": 1, |
|||
"email": 1, |
|||
"print": 1, |
|||
"read": 1, |
|||
"role": "System Manager", |
|||
"share": 1, |
|||
"write": 1 |
|||
} |
|||
], |
|||
"quick_entry": 1, |
|||
"sort_field": "modified", |
|||
"sort_order": "DESC", |
|||
"track_changes": 1 |
|||
} |
@ -0,0 +1,157 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors |
|||
# For license information, please see license.txt |
|||
|
|||
from __future__ import unicode_literals |
|||
import frappe, requests, json |
|||
from frappe import _ |
|||
from frappe.utils import get_site_url, get_url_to_form, get_link_to_form |
|||
from frappe.model.document import Document |
|||
from frappe.utils.file_manager import get_file, get_file_path |
|||
from six.moves.urllib.parse import urlencode |
|||
class LinkedInSettings(Document): |
|||
def get_authorization_url(self): |
|||
params = urlencode({ |
|||
"response_type":"code", |
|||
"client_id": self.consumer_key, |
|||
"redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback", |
|||
"scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social" |
|||
}) |
|||
|
|||
url = "https://www.linkedin.com/oauth/v2/authorization?{}".format(params) |
|||
|
|||
return url |
|||
|
|||
def get_access_token(self, code): |
|||
url = "https://www.linkedin.com/oauth/v2/accessToken" |
|||
body = { |
|||
"grant_type": "authorization_code", |
|||
"code": code, |
|||
"client_id": self.consumer_key, |
|||
"client_secret": self.get_password(fieldname="consumer_secret"), |
|||
"redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback", |
|||
} |
|||
headers = { |
|||
"Content-Type": "application/x-www-form-urlencoded" |
|||
} |
|||
|
|||
response = self.http_post(url=url, data=body, headers=headers) |
|||
response = frappe.parse_json(response.content.decode()) |
|||
self.db_set("access_token", response["access_token"]) |
|||
|
|||
def get_member_profile(self): |
|||
headers = { |
|||
"Authorization": "Bearer {}".format(self.access_token) |
|||
} |
|||
url = "https://api.linkedin.com/v2/me" |
|||
response = requests.get(url=url, headers=headers) |
|||
response = frappe.parse_json(response.content.decode()) |
|||
self.db_set("person_urn", response["id"]) |
|||
self.db_set("account_name", response["vanityName"]) |
|||
self.db_set("session_status", "Active") |
|||
frappe.local.response["type"] = "redirect" |
|||
frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") |
|||
|
|||
def post(self, text, media=None): |
|||
if not media: |
|||
return self.post_text(text) |
|||
else: |
|||
media_id = self.upload_image(media) |
|||
if media_id: |
|||
return self.post_text(text, media_id=media_id) |
|||
else: |
|||
frappe.log_error("Failed to upload media.","LinkedIn Upload Error") |
|||
|
|||
|
|||
def upload_image(self, media): |
|||
media = get_file_path(media) |
|||
register_url = "https://api.linkedin.com/v2/assets?action=registerUpload" |
|||
|
|||
body = { |
|||
"registerUploadRequest": { |
|||
"recipes": ["urn:li:digitalmediaRecipe:feedshare-image"], |
|||
"owner": "urn:li:organization:{0}".format(self.company_id), |
|||
"serviceRelationships": [{ |
|||
"relationshipType": "OWNER", |
|||
"identifier": "urn:li:userGeneratedContent" |
|||
}] |
|||
} |
|||
} |
|||
headers = { |
|||
"Authorization": "Bearer {}".format(self.access_token) |
|||
} |
|||
response = self.http_post(url=register_url, body=body, headers=headers) |
|||
if response.status_code == 200: |
|||
response = response.json() |
|||
asset = response["value"]["asset"] |
|||
upload_url = response["value"]["uploadMechanism"]["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"]["uploadUrl"] |
|||
headers['Content-Type']='image/jpeg' |
|||
response = self.http_post(upload_url, headers=headers, data=open(media,"rb")) |
|||
if response.status_code < 200 and response.status_code > 299: |
|||
frappe.throw(_("Error While Uploading Image"), title="{0} {1}".format(response.status_code, response.reason)) |
|||
return None |
|||
return asset |
|||
return None |
|||
|
|||
|
|||
def post_text(self, text, media_id=None): |
|||
# url = "https://api.linkedin.com/v2/ugcPosts" |
|||
url = "https://api.linkedin.com/v2/shares" |
|||
headers = { |
|||
"X-Restli-Protocol-Version": "2.0.0", |
|||
"Authorization": "Bearer {}".format(self.access_token), |
|||
"Content-Type": "application/json; charset=UTF-8" |
|||
} |
|||
body = { |
|||
"distribution": { |
|||
"linkedInDistributionTarget": {} |
|||
}, |
|||
"owner":"urn:li:organization:{0}".format(self.company_id), |
|||
"subject": "Test Share Subject", |
|||
"text": { |
|||
"text": text |
|||
} |
|||
} |
|||
if media_id: |
|||
body["content"]= { |
|||
"contentEntities": [{ |
|||
"entity": media_id |
|||
}], |
|||
"shareMediaCategory": "IMAGE" |
|||
} |
|||
response = self.http_post(url=url, headers=headers, body=body) |
|||
return response |
|||
|
|||
def http_post(self, url, headers=None, body=None, data=None): |
|||
try: |
|||
response = requests.post( |
|||
url = url, |
|||
json = body, |
|||
data = data, |
|||
headers = headers |
|||
) |
|||
if response.status_code not in [201,200]: |
|||
raise |
|||
except Exception as e: |
|||
content = json.loads(response.content) |
|||
if response.status_code == 401: |
|||
self.db_set("session_status", "Expired") |
|||
frappe.db.commit() |
|||
frappe.throw(content["message"], title="LinkedIn Error - Unauthorized") |
|||
elif response.status_code == 403: |
|||
frappe.msgprint(_("You Didn't have permission to access this API")) |
|||
frappe.throw(content["message"], title="LinkedIn Error - Access Denied") |
|||
else: |
|||
frappe.throw(response.reason, title=response.status_code) |
|||
return response |
|||
|
|||
@frappe.whitelist() |
|||
def callback(code=None, error=None, error_description=None): |
|||
if not error: |
|||
linkedin_settings = frappe.get_doc("LinkedIn Settings") |
|||
linkedin_settings.get_access_token(code) |
|||
linkedin_settings.get_member_profile() |
|||
frappe.db.commit() |
|||
else: |
|||
frappe.local.response["type"] = "redirect" |
|||
frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") |
@ -0,0 +1,10 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors |
|||
# See license.txt |
|||
from __future__ import unicode_literals |
|||
|
|||
# import frappe |
|||
import unittest |
|||
|
|||
class TestLinkedInSettings(unittest.TestCase): |
|||
pass |
@ -0,0 +1,54 @@ |
|||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
|||
// For license information, please see license.txt
|
|||
frappe.ui.form.on('Social Media Post', { |
|||
validate: function(frm){ |
|||
if(frm.doc.text.length > 280){ |
|||
frappe.throw("Length Must be less than 280.") |
|||
} |
|||
}, |
|||
refresh: function(frm){ |
|||
if(frm.doc.docstatus === 1){ |
|||
if(frm.doc.post_status != "Posted"){ |
|||
add_post_btn(frm); |
|||
} |
|||
else if(frm.doc.post_status == "Posted"){ |
|||
frm.set_df_property('sheduled_time', 'read_only', 1); |
|||
} |
|||
let html = '<div class="row">'; |
|||
if(frm.doc.twitter){ |
|||
let color = frm.doc.twitter_post_id ? "green" : "red"; |
|||
let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; |
|||
html += '<div class="col-xs-6">' + |
|||
'<span class="indicator whitespace-nowrap '+ color +'"><span class="hidden-xs">Twitter : '+ status +'</span></span> ' + |
|||
'</div>' ; |
|||
} |
|||
if(frm.doc.linkedin){ |
|||
let color = frm.doc.linkedin_post_id ? "green" : "red"; |
|||
let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; |
|||
html += '<div class="col-xs-6">' + |
|||
'<span class="indicator whitespace-nowrap '+ color +'"><span class="hidden-xs">LinkedIn : '+ status +'</span></span> ' + |
|||
'</div>' ; |
|||
} |
|||
html += '</div>'; |
|||
frm.dashboard.set_headline_alert(html); |
|||
} |
|||
} |
|||
}); |
|||
var add_post_btn = function(frm){ |
|||
frm.add_custom_button(('Post Now'), function(){ |
|||
post(frm); |
|||
}); |
|||
} |
|||
var post = function(frm){ |
|||
frappe.call({ |
|||
method: "erpnext.crm.doctype.social_media_post.social_media_post.publish", |
|||
args: { |
|||
doctype: frm.doc.doctype, |
|||
name: frm.doc.name |
|||
}, |
|||
callback: function(r) { |
|||
frm.reload_doc(); |
|||
} |
|||
}) |
|||
|
|||
} |
@ -0,0 +1,157 @@ |
|||
{ |
|||
"actions": [], |
|||
"autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}", |
|||
"creation": "2020-01-30 11:53:13.872864", |
|||
"doctype": "DocType", |
|||
"editable_grid": 1, |
|||
"engine": "InnoDB", |
|||
"field_order": [ |
|||
"campaign_name", |
|||
"sheduled_time", |
|||
"post_status", |
|||
"column_break_6", |
|||
"twitter", |
|||
"linkedin", |
|||
"twitter_post_id", |
|||
"linkedin_post_id", |
|||
"content", |
|||
"text", |
|||
"column_break_14", |
|||
"tweet_preview", |
|||
"linkedin_section", |
|||
"linkedin_post", |
|||
"attachments_section", |
|||
"image", |
|||
"amended_from" |
|||
], |
|||
"fields": [ |
|||
{ |
|||
"fieldname": "text", |
|||
"fieldtype": "Small Text", |
|||
"label": "Tweet", |
|||
"reqd": 1 |
|||
}, |
|||
{ |
|||
"fieldname": "image", |
|||
"fieldtype": "Attach Image", |
|||
"label": "Image" |
|||
}, |
|||
{ |
|||
"default": "0", |
|||
"fieldname": "twitter", |
|||
"fieldtype": "Check", |
|||
"label": "Twitter" |
|||
}, |
|||
{ |
|||
"default": "0", |
|||
"fieldname": "linkedin", |
|||
"fieldtype": "Check", |
|||
"label": "LinkedIn" |
|||
}, |
|||
{ |
|||
"fieldname": "amended_from", |
|||
"fieldtype": "Link", |
|||
"label": "Amended From", |
|||
"no_copy": 1, |
|||
"options": "Social Media Post", |
|||
"print_hide": 1, |
|||
"read_only": 1 |
|||
}, |
|||
{ |
|||
"allow_on_submit": 1, |
|||
"fieldname": "sheduled_time", |
|||
"fieldtype": "Datetime", |
|||
"label": "Scheduled Time" |
|||
}, |
|||
{ |
|||
"fieldname": "content", |
|||
"fieldtype": "Section Break", |
|||
"label": "Twitter" |
|||
}, |
|||
{ |
|||
"allow_on_submit": 1, |
|||
"fieldname": "post_status", |
|||
"fieldtype": "Select", |
|||
"label": "Post Status", |
|||
"options": "\nScheduled\nPosted\nError", |
|||
"read_only": 1 |
|||
}, |
|||
{ |
|||
"allow_on_submit": 1, |
|||
"fieldname": "twitter_post_id", |
|||
"fieldtype": "Data", |
|||
"hidden": 1, |
|||
"label": "Twitter Post Id", |
|||
"read_only": 1 |
|||
}, |
|||
{ |
|||
"allow_on_submit": 1, |
|||
"fieldname": "linkedin_post_id", |
|||
"fieldtype": "Data", |
|||
"hidden": 1, |
|||
"label": "LinkedIn Post Id", |
|||
"read_only": 1 |
|||
}, |
|||
{ |
|||
"fieldname": "campaign_name", |
|||
"fieldtype": "Link", |
|||
"in_list_view": 1, |
|||
"label": "Campaign", |
|||
"options": "Campaign" |
|||
}, |
|||
{ |
|||
"fieldname": "column_break_6", |
|||
"fieldtype": "Column Break", |
|||
"label": "Share On" |
|||
}, |
|||
{ |
|||
"fieldname": "column_break_14", |
|||
"fieldtype": "Column Break" |
|||
}, |
|||
{ |
|||
"fieldname": "tweet_preview", |
|||
"fieldtype": "HTML" |
|||
}, |
|||
{ |
|||
"collapsible": 1, |
|||
"fieldname": "linkedin_section", |
|||
"fieldtype": "Section Break", |
|||
"label": "LinkedIn" |
|||
}, |
|||
{ |
|||
"collapsible": 1, |
|||
"fieldname": "attachments_section", |
|||
"fieldtype": "Section Break", |
|||
"label": "Attachments" |
|||
}, |
|||
{ |
|||
"fieldname": "linkedin_post", |
|||
"fieldtype": "Text", |
|||
"label": "Post" |
|||
} |
|||
], |
|||
"is_submittable": 1, |
|||
"links": [], |
|||
"modified": "2020-04-09 22:35:23.821991", |
|||
"modified_by": "Administrator", |
|||
"module": "CRM", |
|||
"name": "Social Media Post", |
|||
"owner": "Administrator", |
|||
"permissions": [ |
|||
{ |
|||
"create": 1, |
|||
"delete": 1, |
|||
"email": 1, |
|||
"export": 1, |
|||
"print": 1, |
|||
"read": 1, |
|||
"report": 1, |
|||
"role": "System Manager", |
|||
"share": 1, |
|||
"write": 1 |
|||
} |
|||
], |
|||
"sort_field": "modified", |
|||
"sort_order": "DESC", |
|||
"track_changes": 1 |
|||
} |
@ -0,0 +1,49 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors |
|||
# For license information, please see license.txt |
|||
|
|||
from __future__ import unicode_literals |
|||
import frappe |
|||
from frappe.model.document import Document |
|||
from frappe import _ |
|||
|
|||
class SocialMediaPost(Document): |
|||
def submit(self): |
|||
if self.sheduled_time: |
|||
self.post_status = "Scheduled" |
|||
super(SocialMediaPost, self).submit() |
|||
|
|||
def post(self): |
|||
try: |
|||
if self.twitter and not self.twitter_post_id: |
|||
twitter = frappe.get_doc("Twitter Settings") |
|||
twitter_post = twitter.post(self.text, self.image) |
|||
self.db_set("twitter_post_id", twitter_post.id) |
|||
if self.linkedin and not self.linkedin_post_id: |
|||
linkedin = frappe.get_doc("LinkedIn Settings") |
|||
linkedin_post = linkedin.post(self.text, self.image) |
|||
self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1]) |
|||
self.db_set("post_status", "Posted") |
|||
|
|||
except Exception as e: |
|||
self.db_set("post_status", "Error") |
|||
title = _("Error while POSTING {0}").format(self.name) |
|||
traceback = frappe.get_traceback() |
|||
frappe.log_error(message=traceback , title=title) |
|||
|
|||
def process_scheduled_social_media_posts(): |
|||
import datetime |
|||
posts = frappe.get_list("Social Media Post", filters={"status": "Scheduled"}, fields= ["name", "sheduled_time"]) |
|||
start = frappe.utils.now_datetime() |
|||
end = start + datetime.timedelta(minutes=59) |
|||
for post in posts: |
|||
post_time = frappe.utils.get_datetime(post.scheduled_time) |
|||
if post_time > start and post_time <= end: |
|||
post = frappe.get_doc('Social Media Post',post['name']) |
|||
post.post() |
|||
|
|||
@frappe.whitelist() |
|||
def publish(doctype, name): |
|||
sm_post = frappe.get_doc(doctype, name) |
|||
sm_post.post() |
|||
frappe.db.commit() |
@ -0,0 +1,10 @@ |
|||
frappe.listview_settings['Social Media Post'] = { |
|||
add_fields: ["status","post_status"], |
|||
get_indicator: function(doc) { |
|||
return [__(doc.post_status), { |
|||
"Scheduled": "orange", |
|||
"Posted": "green", |
|||
"Error": "red" |
|||
}[doc.post_status]]; |
|||
} |
|||
} |
@ -0,0 +1,10 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors |
|||
# See license.txt |
|||
from __future__ import unicode_literals |
|||
|
|||
# import frappe |
|||
import unittest |
|||
|
|||
class TestSocialMediaPost(unittest.TestCase): |
|||
pass |
@ -0,0 +1,10 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors |
|||
# See license.txt |
|||
from __future__ import unicode_literals |
|||
|
|||
# import frappe |
|||
import unittest |
|||
|
|||
class TestTwitterSettings(unittest.TestCase): |
|||
pass |
@ -0,0 +1,52 @@ |
|||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
|||
// For license information, please see license.txt
|
|||
|
|||
frappe.ui.form.on('Twitter Settings', { |
|||
onload: function(frm){ |
|||
if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ |
|||
frappe.confirm( |
|||
'Session not valid, Do you want to login?', |
|||
function(){ |
|||
frm.trigger("login"); |
|||
}, |
|||
function(){ |
|||
window.close(); |
|||
} |
|||
) |
|||
} |
|||
}, |
|||
refresh: function(frm){ |
|||
if(frm.doc.session_status=="Active"){ |
|||
frm.dashboard.set_headline_alert( |
|||
'<div class="row">' + |
|||
'<div class="col-xs-12">' + |
|||
'<span class="indicator whitespace-nowrap green'+ '' +'"><span class="hidden-xs">Session Active</span></span> ' + |
|||
'</div>' + |
|||
'</div>' |
|||
); |
|||
} |
|||
else if(frm.doc.session_status=="Expired"){ |
|||
frm.dashboard.set_headline_alert( |
|||
'<div class="row">' + |
|||
'<div class="col-xs-12">' + |
|||
'<span class="indicator whitespace-nowrap red'+ '' +'"><span class="hidden-xs">Session Not Active. Save doc to login.</span></span> ' + |
|||
'</div>' + |
|||
'</div>' |
|||
); |
|||
} |
|||
}, |
|||
login: function(frm){ |
|||
if(frm.doc.consumer_key && frm.doc.consumer_secret){ |
|||
frappe.call({ |
|||
doc: frm.doc, |
|||
method: "get_authorize_url", |
|||
callback : function(r) { |
|||
window.location.href = r.message; |
|||
} |
|||
}); |
|||
} |
|||
}, |
|||
after_save: function(frm){ |
|||
frm.trigger("login"); |
|||
} |
|||
}); |
@ -0,0 +1,101 @@ |
|||
{ |
|||
"actions": [], |
|||
"creation": "2020-01-30 10:29:08.562108", |
|||
"doctype": "DocType", |
|||
"editable_grid": 1, |
|||
"engine": "InnoDB", |
|||
"field_order": [ |
|||
"account_name", |
|||
"profile_pic", |
|||
"oauth_details", |
|||
"consumer_key", |
|||
"column_break_5", |
|||
"consumer_secret", |
|||
"oauth_token", |
|||
"oauth_secret", |
|||
"session_status" |
|||
], |
|||
"fields": [ |
|||
{ |
|||
"fieldname": "account_name", |
|||
"fieldtype": "Data", |
|||
"label": "Account Name", |
|||
"read_only": 1 |
|||
}, |
|||
{ |
|||
"fieldname": "oauth_details", |
|||
"fieldtype": "Section Break", |
|||
"label": "OAuth Credentials" |
|||
}, |
|||
{ |
|||
"fieldname": "consumer_key", |
|||
"fieldtype": "Data", |
|||
"in_list_view": 1, |
|||
"label": "Consumer Key", |
|||
"reqd": 1 |
|||
}, |
|||
{ |
|||
"fieldname": "consumer_secret", |
|||
"fieldtype": "Password", |
|||
"in_list_view": 1, |
|||
"label": "Consumer Secret Key", |
|||
"reqd": 1 |
|||
}, |
|||
{ |
|||
"fieldname": "oauth_token", |
|||
"fieldtype": "Data", |
|||
"hidden": 1, |
|||
"label": "OAuth Token", |
|||
"read_only": 1 |
|||
}, |
|||
{ |
|||
"fieldname": "oauth_secret", |
|||
"fieldtype": "Password", |
|||
"hidden": 1, |
|||
"label": "OAuth Token Secret", |
|||
"read_only": 1 |
|||
}, |
|||
{ |
|||
"fieldname": "column_break_5", |
|||
"fieldtype": "Column Break" |
|||
}, |
|||
{ |
|||
"fieldname": "profile_pic", |
|||
"fieldtype": "Attach Image", |
|||
"hidden": 1, |
|||
"read_only": 1 |
|||
}, |
|||
{ |
|||
"fieldname": "session_status", |
|||
"fieldtype": "Select", |
|||
"hidden": 1, |
|||
"label": "Session Status", |
|||
"options": "Expired\nActive", |
|||
"read_only": 1 |
|||
} |
|||
], |
|||
"image_field": "profile_pic", |
|||
"issingle": 1, |
|||
"links": [], |
|||
"modified": "2020-04-08 23:56:20.621246", |
|||
"modified_by": "Administrator", |
|||
"module": "CRM", |
|||
"name": "Twitter Settings", |
|||
"owner": "Administrator", |
|||
"permissions": [ |
|||
{ |
|||
"create": 1, |
|||
"delete": 1, |
|||
"email": 1, |
|||
"print": 1, |
|||
"read": 1, |
|||
"role": "System Manager", |
|||
"share": 1, |
|||
"write": 1 |
|||
} |
|||
], |
|||
"quick_entry": 1, |
|||
"sort_field": "modified", |
|||
"sort_order": "DESC", |
|||
"track_changes": 1 |
|||
} |
@ -0,0 +1,90 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors |
|||
# For license information, please see license.txt |
|||
|
|||
from __future__ import unicode_literals |
|||
import frappe, os, tweepy, json |
|||
from frappe import _ |
|||
from frappe.model.document import Document |
|||
from frappe.utils.file_manager import get_file_path |
|||
from frappe.utils import get_url_to_form, get_link_to_form |
|||
from tweepy.error import TweepError |
|||
|
|||
class TwitterSettings(Document): |
|||
def get_authorize_url(self): |
|||
callback_url = "{0}/?cmd=erpnext.crm.doctype.twitter_settings.twitter_settings.callback".format(frappe.utils.get_url()) |
|||
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url) |
|||
try: |
|||
redirect_url = auth.get_authorization_url() |
|||
return redirect_url |
|||
except: |
|||
frappe.msgprint(_("Error! Failed to get request token.")) |
|||
frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key"))) |
|||
|
|||
|
|||
def get_access_token(self, oauth_token, oauth_verifier): |
|||
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) |
|||
auth.request_token = { |
|||
'oauth_token' : oauth_token, |
|||
'oauth_token_secret' : oauth_verifier |
|||
} |
|||
try: |
|||
auth.get_access_token(oauth_verifier) |
|||
self.db_set("oauth_token", auth.access_token) |
|||
self.db_set("oauth_secret", auth.access_token_secret) |
|||
api = self.get_api() |
|||
user = api.me() |
|||
self.db_set("account_name", user._json["screen_name"]) |
|||
profile_pic = (user._json["profile_image_url"]).replace("_normal","") |
|||
self.db_set("profile_pic", profile_pic) |
|||
self.db_set("session_status", "Active") |
|||
frappe.local.response["type"] = "redirect" |
|||
frappe.local.response["location"] = get_url_to_form("Twitter Settings","Twitter Settings") |
|||
except TweepError as e: |
|||
frappe.msgprint(_("Error! Failed to get access token.")) |
|||
frappe.throw(_('Invalid Consumer Key or Consumer Secret Key')) |
|||
|
|||
def get_api(self): |
|||
# authentication of consumer key and secret |
|||
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) |
|||
# authentication of access token and secret |
|||
auth.set_access_token(self.oauth_token, self.get_password(fieldname="oauth_secret")) |
|||
|
|||
return tweepy.API(auth) |
|||
|
|||
def post(self, text, media=None): |
|||
if not media: |
|||
return self.send_tweet(text) |
|||
|
|||
if media: |
|||
media_id = self.upload_image(media) |
|||
return self.send_tweet(text, media_id) |
|||
|
|||
def upload_image(self, media): |
|||
media = get_file_path(media) |
|||
api = self.get_api() |
|||
media = api.media_upload(media) |
|||
return media.media_id |
|||
|
|||
def send_tweet(self, text, media_id=None): |
|||
api = self.get_api() |
|||
try: |
|||
if media_id: |
|||
response = api.update_status(status = text, media_ids = [media_id]) |
|||
else: |
|||
response = api.update_status(status = text) |
|||
return response |
|||
|
|||
except TweepError as e: |
|||
content = json.loads(e.response.content) |
|||
content = content["errors"][0] |
|||
if e.response.status_code == 401: |
|||
self.db_set("session_status", "Expired") |
|||
frappe.db.commit() |
|||
frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason)) |
|||
|
|||
@frappe.whitelist() |
|||
def callback(oauth_token, oauth_verifier): |
|||
twitter_settings = frappe.get_single("Twitter Settings") |
|||
twitter_settings.get_access_token(oauth_token,oauth_verifier) |
|||
frappe.db.commit() |
Loading…
Reference in new issue